﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using RH.Domain;
using RH.Common.Extensions;
using Lucene.Net.Index;
using Lucene.Net.Store;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Analysis;


namespace RH.Search
{
    /// <summary>
    /// Manages Index for Lucene
    /// </summary>
    public class ProductIndexManager : RH.Search.IIndexManager<Product>
    {
        private IndexWriter _IndexWriter = null;
        AppEntities _Context = null;

        #region Constructors
        public ProductIndexManager() 
        {            
            InitIndexWriter(SearchConstants.ProductIndexDirectory);
        }        
        #endregion Constructors
        
        #region Public Methods
        /// <summary>
        /// This method should use AddToLuceneIndex instead BuildLuceneDocument
        /// </summary>
        /// <param name="product"></param>
        /// <param name="appName">Name of the application which is creating this index</param>
        public void IndexDocument(Product product, String appName)
        {
            StringBuilder dbDocument = new StringBuilder();
            Document doc = BuildLuceneDocument(product, dbDocument);
            _IndexWriter.AddDocument(doc);
            ProductIndex productIndex = _Context.ProductIndexes.FirstOrDefault(P => P.ProductID == product.ID);
            if (productIndex == null)
            {
                productIndex = new ProductIndex();
                _Context.ProductIndexes.AddObject(productIndex);
                //_IndexWriter.Commit();
                //_IndexWriter.Close();
            }
            SetPropertiesOfDbDocument(productIndex, product, dbDocument, appName);
            _Context.SaveChanges();
        }
        
        /// <summary>
        /// Deletes a Lucene Index by product id, as we have configured lucene to think product id as primary key.
        /// </summary>
        /// <param name="productID"></param>
        public void DeleteIndex(int productID)
        {
            _IndexWriter.DeleteDocuments(new Term("id", productID.ToString()));
            ProductIndex productIndex = _Context.ProductIndexes.FirstOrDefault(P => P.ProductID == productID);
            if (productIndex != null)
            {
                _Context.DeleteObject(productIndex);
                _Context.SaveChanges();                
            }
            //_IndexWriter.Commit();
        }

        /// <summary>
        /// Deletes the existing index trace from Lucene and DB then Inserts index in Lucene and DB as well.
        /// </summary>
        /// <param name="product"></param>
        public void UpdateIndex(Product product, String appName)
        {
            DeleteIndex(product.ID);
            IndexDocument(product, appName);
        }

        public void Optimize()
        {
            _IndexWriter.Optimize();
        }
        public void CommitChanges()
        {
            _IndexWriter.Commit();
        }
        /// <summary>
        /// CloseWriter must be called after completing all indexing operation, to write the changes in disk.
        /// </summary>
        public void CloseWriter()
        {
            //_IndexWriter.Close();
            _IndexWriter.Dispose();
        }

        public void Rollback()
        {
            _IndexWriter.Rollback();
        }
        #endregion Public Methods

        #region Private Methods

        /// <summary>
        /// Initializes Index Writer and Configures it index documents.
        /// </summary>
        /// <param name="indexDirectory"></param>
        private void InitIndexWriter(String indexDirectory)
        {
            if (String.IsNullOrEmpty(indexDirectory))
                throw new ArgumentException("Index directory cannot be null");

            if (!System.IO.Directory.Exists(indexDirectory))
                System.IO.Directory.CreateDirectory(indexDirectory);

            Lucene.Net.Store.Directory directory = Lucene.Net.Store.FSDirectory.Open(indexDirectory);
            _IndexWriter = new IndexWriter(directory, 
                    new WhitespaceAnalyzer(),
                    //new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30), 
                    IndexWriter.MaxFieldLength.UNLIMITED);

            _Context = new AppEntities();  
        }

        private void AddToLuceneIndex(Document doc)
        {
            _IndexWriter.AddDocument(doc);
        }

        #endregion Private Methods

        #region Re-Index Products
        
        /// <summary>
        /// Deletes existing indexes and rebuilds index in DB and Lucene
        /// </summary>
        /// <returns></returns>
        public ReIndexResult ReIndex()
        {
            ReIndexResult result = new ReIndexResult();
            var products = (from P in _Context.Products.Include("Category").Include("Category.ParentCategory").Include("Category.ChildCategories")
                           where P.Company.IsActive
                               && P.IsActive
                               && !P.IsDeleted
                           select P).ToList();
            result.FoundItems = products.Count();

            ///Delete All ProductIndexes from DB and Lucene Index
            foreach (ProductIndex productIndex in _Context.ProductIndexes)
            {
                _IndexWriter.DeleteDocuments(new Term("id", productIndex.ProductID.ToString()));
                _Context.ProductIndexes.DeleteObject(productIndex);
                result.DeletedIndexFromDB = result.DeletedIndexFromLucene = result.DeletedIndexFromLucene + 1;
            }
            this.CommitChanges();
            _Context.SaveChanges();            

            StringBuilder dbDocument = new StringBuilder();
            foreach (Product product in products)
            {
                dbDocument.Length = 0;
                //Prepare Lucene Document                
                Document luceneDocument = BuildLuceneDocument(product, dbDocument);
                ///Already deleted all Lucene index documents, so no need to delete them again
                ///Add new Document to Lucene Index
                AddToLuceneIndex(luceneDocument);
                //Update Index Document of DB with latest changes
                ProductIndex productIndex = _Context.ProductIndexes.FirstOrDefault(P => P.ProductID == product.ID);
                if (productIndex == null)
                {
                    productIndex = new ProductIndex();
                    _Context.ProductIndexes.AddObject(productIndex);
                }
                SetPropertiesOfDbDocument(productIndex, product, dbDocument, "ProductIndexManager.ReIndex");
                result.ReIndexedItems += 1;             
            }
            //Save new Document to DB
            _Context.SaveChanges();
            this.CommitChanges();
            this.Optimize();
            this.CloseWriter();
            return result;
        }
        /// <summary>
        /// Builds a Lucene document for indexing 
        /// along with, creates a similar document to save in the database as well.
        /// </summary>
        /// <param name="product">The Product object to be indexed</param>
        /// <param name="sb">The reference of the DB document. Which will be updated with new document properties</param>
        /// <returns>Returns a Lucene Document</returns>
        private Document BuildLuceneDocument(Product product, StringBuilder sb)
        {            
            Document doc = new Document();
            String category = BuildCategoryPath(product);
            String location = BuildLocationPath(product);
            String price = SearchConstants.DECIMAL_ZERO.ToString(SearchConstants.LUCENE_DECIMAL_FORMAT);
            if(product.Price.HasValue)
                price = product.Price.Value.ToString(SearchConstants.LUCENE_DECIMAL_FORMAT);

            doc.Add(new Field("id", product.ID.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
            doc.Add(new Field("for_rent", product.IsForRent ? "1" : "0", Field.Store.YES, Field.Index.NOT_ANALYZED));
            doc.Add(new Field("location", location.ToUpper(), Field.Store.NO, Field.Index.ANALYZED));
            doc.Add(new Field("category", category.ToUpper(), Field.Store.NO, Field.Index.ANALYZED));
            doc.Add(new Field("category_id", product.CategoryID.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
            doc.Add(new Field("title", product.Name.ToUpper(), Field.Store.NO, Field.Index.ANALYZED));
            doc.Add(new Field("title_orig", product.Name, Field.Store.YES, Field.Index.NOT_ANALYZED));
            doc.Add(new Field("description", product.Description.ToUpper(), Field.Store.NO, Field.Index.ANALYZED));
            
            //if(!product.Model.IsNullOrEmpty())
            //    doc.Add(new Field("model", product.Model.ToUpper(), Field.Store.NO, Field.Index.ANALYZED));
            doc.Add(new Field("companyid", product.CompanyID.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
            doc.Add(new Field("company", product.Company.Name.ToUpper(), Field.Store.NO, Field.Index.ANALYZED));

            if (!product.Price.HasValue)
                product.Price = 0;            
            NumericField priceField = new NumericField("price", Field.Store.YES, true);
            priceField.SetDoubleValue((double)product.Price);
            //doc.Add(new Field("price", price, Field.Store.YES, Field.Index.NOT_ANALYZED));
            doc.Add(priceField);

            doc.Add(new Field("created", 
                    DateTools.DateToString(product.Created, DateTools.Resolution.SECOND), 
                    Field.Store.YES, 
                    Field.Index.NOT_ANALYZED));
            doc.Add(new Field("modified",
                    DateTools.DateToString(product.Modified, DateTools.Resolution.SECOND),
                    Field.Store.YES,
                    Field.Index.NOT_ANALYZED));
            doc.Add(new Field("full_content", String.Format("{0} {1} {2} {3} {4}", 
                    category.ToUpper(), location.ToUpper(), product.Company.Name.ToUpper(), product.Name.ToUpper(), product.Description.ToUpper())
                    , Field.Store.NO, Field.Index.ANALYZED));

            ///Document to save in DB
            sb.AppendFormat("id:{0}{1}", product.ID, Environment.NewLine);
            sb.AppendFormat("for_rent:{0}{1}", product.IsForRent ? "1" : "0", Environment.NewLine);
            sb.AppendFormat("category:{0}{1}", category.ToUpper(), Environment.NewLine);
            sb.AppendFormat("category_id:{0}{1}", product.CategoryID.ToString(), Environment.NewLine);
            sb.AppendFormat("location:{0}", location.ToUpper(), Environment.NewLine);
            sb.AppendFormat("title:{0}{1}", product.Name.RemoveNewline().ToUpper(), Environment.NewLine);
            sb.AppendFormat("title_orig:{0}{1}", product.Name, Environment.NewLine);
            sb.AppendFormat("description:{0}{1}", product.Description.RemoveNewline().ToUpper(), Environment.NewLine);
            //if (!product.Model.IsNullOrEmpty())
            //    sb.AppendFormat("model:{0}{1}", product.Model.RemoveNewline(), Environment.NewLine);
            sb.AppendFormat("companyid:{0}{1}", product.CompanyID, Environment.NewLine);
            sb.AppendFormat("company:{0}{1}", product.Company.Name.RemoveNewline().ToUpper(), Environment.NewLine);
            //sb.AppendFormat("price:{0}{1}", price, Environment.NewLine);
            sb.AppendFormat("price:{0}{1}", ((double)product.Price).ToString(), Environment.NewLine);
            sb.AppendFormat("created:{0}{1}", DateTools.DateToString(product.Created, DateTools.Resolution.SECOND), Environment.NewLine);
            sb.AppendFormat("modified:{0}{1}", DateTools.DateToString(product.Modified, DateTools.Resolution.SECOND), Environment.NewLine);
            sb.AppendFormat("full_content:{0}{1}", String.Format("{0} {1} {2} {3}", category.ToUpper(), product.Company.Name.ToUpper(), product.Name.ToUpper(), product.Description.ToUpper()), Environment.NewLine);

            ///Parse ZGL Payload Metadata
            XDocument xmlDoc = XDocument.Parse(product.ZGLPayload);
            var properties = xmlDoc.Root.Elements("Property");
            foreach (var property in properties)
            {
                String fieldName = GetAttributeValue(property, "Name"); //property.Attribute("Name").Value;
                String fieldValue = GetAttributeValue(property, "Value"); //property.Attribute("Value").Value;
                if (!fieldName.IsNullOrEmpty() && !fieldValue.IsNullOrEmpty())
                {
                    if (fieldValue.IsNumeric())
                    {
                        NumericField numField = new NumericField(fieldName.ToLower(), Field.Store.YES, true);
                        numField.SetDoubleValue(fieldValue.ToDouble());
                        doc.Add(numField);
                        //doc.Add(new Field(fieldName, fieldValue, Field.Store.YES, Field.Index.NOT_ANALYZED));
                    }
                    else
                        doc.Add(new Field(fieldName.ToLower(), fieldValue.ToUpper(), Field.Store.NO, Field.Index.ANALYZED));

                    sb.AppendFormat("{0}:{1}{2}", fieldName.ToLower(), fieldValue.RemoveNewline().ToUpper(), Environment.NewLine);
                }
            }
            return doc;
        }

        private string GetAttributeValue(XElement property, string attributeName)
        {
            if (property != null)
            {
                var attribute = property.Attribute(attributeName);
                if (attribute != null)
                    return attribute.Value;
            }
            return String.Empty;
        }
        /// <summary>
        /// Sets properties of the productIndex object to save in the databasse
        /// </summary>
        /// <param name="productIndex"></param>
        /// <param name="product"></param>
        /// <param name="dbDocument"></param>
        /// <param name="appName"></param>
        private void SetPropertiesOfDbDocument(ProductIndex productIndex, Product product, StringBuilder dbDocument, String appName)
        {
            productIndex.ProductID = product.ID;
            productIndex.Name = product.Name;
            productIndex.Description = product.Description;
            //productIndex.Model = product.Model;
            productIndex.XMLData = dbDocument.ToString();
            productIndex.IndexUpdated = DateTime.Now;
            productIndex.HasInLucene = true;
            productIndex.AppName = appName;
        }

        /// <summary>
        /// Builds a category path for a given product object.
        /// TODO: This should be recursive function. Currently it assumes the sub-category depth is 3
        /// </summary>
        /// <param name="product"></param>
        /// <returns>Returns a category string of the format /{parent}/{child1}/{child2}</returns>
        private string BuildCategoryPath(Product product)
        {
            StringBuilder sb = new StringBuilder(5);
            sb.AppendFormat("/ {0} ", product.Category.Name);
            if (product.Category.ParentCategory != null)
            {
                sb.AppendFormat("/ {0} ", product.Category.ParentCategory.Name);
                if(product.Category.ParentCategory.ParentCategory != null)
                    sb.AppendFormat("/ {0} ", product.Category.ParentCategory.ParentCategory.Name);
            }
            return sb.ToString();
        }

        /// <summary>
        /// Builds Location Path to Store in Lucene Document, if exists
        /// </summary>
        /// <param name="product"></param>
        /// <returns></returns>
        private String BuildLocationPath(Product product)
        {   
            if (product.Address != null && product.Address.Location != null)
            {
                return String.Format("{0} / {1} / {2}",
                    product.Address.Location.District.Country.Name,
                    product.Address.Location.District.Name,
                    product.Address.Location.Name);
            }
            return String.Empty;
        }
        #endregion
    }
}
